from tronpy import AsyncTron, Tron, providers
from tronpy.keys import PrivateKey
import random
import string
import asyncio
import httpx
import time


def current_milli_time():
    return round(time.time() * 1000)


class PaymentSystem:
    def __init__(self, priv_key, logger):
        self.priv_key = PrivateKey(bytes.fromhex(priv_key))
        self.publ_key = self.priv_key.public_key.to_base58check_address()
        self.invoice_storage = {}
        self.user_invoice = {}  # userid:{amount,extra}
        self.usdt_address = 'q'  # mainnet
        self.royalty = 10  # %
        self.DECIMALS = 6
        self.MIN_EXTRA = 0
        self.MAX_EXTRA = 10**self.DECIMALS-1
        self.DURATION = 1 * 3600 * 1000  # 1 hour should be
        self.logger = logger

    # {
    # +	userid
    # +	completed
    # 	timestamp_issued
    # 	timestamp_finished
    # +	timestamp_deadline
    # 	amount
    # 	tx
    # }

    def get_address(self):
        return self.publ_key

    async def send_usdt_royalty(self, value, receiver_address):
        self.logger.debug(f'Выводятся {value} денег на {receiver_address}')
        old_value = value
        try:
            async with AsyncTron(provider=providers.AsyncHTTPProvider(api_key=), network='mainnet') as client:
                usdt_contract = await client.get_contract(self.usdt_address)
                decimals = await usdt_contract.functions.decimals()

                value = int(value*(100-self.royalty)/100 * 10 ** decimals)
                txn = (
                    await usdt_contract.functions.transfer(receiver_address, value)
                )
                # txn = txn.memo("MONEY GIGLE GIGLE")
                txn = txn.with_owner(self.publ_key)
                txn = await txn.build()
                txn_ret = await txn.sign(self.priv_key).broadcast()

                self.logger.debug(
                    f'Вывелись {old_value} денег на {receiver_address} транза - {txn_ret.txid}')
                return txn_ret.txid
        except Exception as err:
            self.logger.debug(
                f'Ошибка {value} денег на {receiver_address} причина - '+str(err))
        return 0

    async def get_bank(self):
        final_text = ''
        link = f'https://apilist.tronscan.org/api/account/tokens?address={self.publ_key}&start=0&limit=20&token=&hidden=0&show=0&sortType=0'
        try:
            async with httpx.AsyncClient() as client:
                r = await client.get(link)
                data = r.json()['data']
                for token in data:
                    token_info = f"{token['quantity']} {token['tokenAbbr']}\n"
                    final_text += token_info
                return final_text
        except Exception as err:
            return err

    def merge_amount_extra(self, amount, extra):
        return f'{amount}{str(extra).zfill(self.DECIMALS)}'

    # 0 - valid, 1 - can replace, -1 can't replace
    @staticmethod
    def check_invoice(userid, invoice):
        if not invoice:  # пустой инвойс
            return 1
        userid = str(userid)
        current_time = current_milli_time()
        # если инвойс просрочен, его можно заменить
        if invoice['timestamp_deadline'] <= current_time:
            return 1
        # если инвойс завершен, его можно заменить
        if invoice['completed'] == True:
            return 1
        # если это чужой инвойс, то не трогай блять
        if invoice['userid'] != userid:
            return -1
        # если все выше ок, то это валидный инвойс юзера, но еще не исполненный
        return 0

    # находит доступное добавление
    def find_available_extra(self, userid, amount):  # returns extra or None
        # инициализация если вдруг число не юзалось ранее
        if amount not in self.invoice_storage:
            self.invoice_storage[amount] = {}
            self.logger.debug(f'Данная сумма {amount} еще не использовалась')
        # ищем свободное место для инвойса
        for extra in range(self.MIN_EXTRA, self.MAX_EXTRA):
            # уперлись в свободное пространство
            not_in_storage = extra not in self.invoice_storage[amount]
            if not_in_storage:
                self.invoice_storage[amount][extra] = {}
                self.logger.debug(
                    f'Данное extra {extra} при сумме {amount} можно занять т.к. not_in_storage={not_in_storage}, берем его')
                return extra
            # или инвойс доступен для занятия
            check_invoice = PaymentSystem.check_invoice(
                userid, self.invoice_storage[amount][extra])
            if check_invoice == 1 or check_invoice == 0:
                self.invoice_storage[amount][extra] = {}
                self.logger.debug(
                    f'Данное extra {extra} при сумме {amount} можно занять т.к. not_in_storage={not_in_storage} check_invoice={check_invoice}, берем его')
                return extra

                # мы не нашли свободного места для инвойса
        self.logger.debug(
            f'Мы не нашли свободного места для {amount}')
        return None

    def remove_extra(self, userid, amount, extra):
        # эта сумма все еще не использовалась
        if not self.invoice_storage[amount]:
            return 'эта сумма все еще не использовалась'
        # на этом месте пустой инвойс
        if not self.invoice_storage[amount][extra]:
            return 'на этом месте пустой инвойс'
        # это инвойс не нашего юзера, не трогаем
        if self.invoice_storage[amount][extra]['userid'] != userid:
            return 'это инвойс не нашего юзера, не трогаем'
        else:  # это инвойс юзера, его надо удалить
            self.invoice_storage[amount].pop(extra)
        return 'это инвойс юзера, его надо удалить'

    def try_create_invoice(self, userid, amount):
        self.logger.debug(
            f'Пользователь {userid} хочет инвойс на сумму {amount}')
        userid = str(userid)
        # у юзера есть какой-то инвойс, где надо просто удалить
        if userid in self.user_invoice:
            prev_amount = self.user_invoice[userid]['amount']
            prev_extra = self.user_invoice[userid]['extra']
            self.logger.debug(
                f'У пользователя {userid} есть какой-то инвойс {prev_amount} {prev_extra}')
            comment = self.remove_extra(userid, prev_amount, prev_extra)
            self.logger.debug(
                f'Результат удаления инвойса у {userid}: {comment}')
        # получили место для инвойса, создаем инвойс
        found_extra = self.find_available_extra(userid, amount)
        self.logger.debug(
            f'Пользователь {userid} получил место для инвойса {amount} {found_extra}')
        # если не нашли места
        if found_extra is None:
            return None, None
        # если нашли, указываем в хранилище юзера его активный инвойс
        self.user_invoice[userid] = {
            'amount': amount,
            'extra': found_extra
        }
        # создали строку с бабками с учетом decimals
        total_amount = self.merge_amount_extra(amount, found_extra)

        timestamp_issued = current_milli_time()
        timestamp_finished = 0
        timestamp_deadline = timestamp_issued + self.DURATION

        invoice = {
            'userid': userid,
            'completed': False,
            'timestamp_issued': timestamp_issued,
            'timestamp_finished': timestamp_finished,
            'timestamp_deadline': timestamp_deadline,
            'amount': total_amount
        }

        self.invoice_storage[amount][found_extra] = invoice
        return amount, found_extra

    async def check_invoice_status(self, userid):
        userid = str(userid)
        # у чела нет никаких инвойсов
        if userid not in self.user_invoice:
            self.logger.debug(f'У пользователя {userid} нет никаких инвойсов')
            return None
        invoice_data = self.user_invoice[userid]
        amount = invoice_data['amount']
        extra = invoice_data['extra']
        self.logger.debug(
            f'У пользователя {userid} есть инвойс с amount={amount} extra={extra}')
        if amount not in self.invoice_storage:
            self.logger.debug(f'У нас нет инвойсов с amount={amount}')
            return None
        if extra not in self.invoice_storage[amount]:
            self.logger.debug(
                f'У нас нет инвойсов с amount={amount} в которых есть extra={extra}')
            return None
        that_invoice = self.invoice_storage[amount][extra]
        self.logger.debug(
            f'Нашли вот такой инвойс: {that_invoice}')
        check_invoice_result = self.check_invoice(userid, that_invoice)
        self.logger.debug(
            f'Результат проверки инвойса: {check_invoice_result}')
        # 0 - valid, 1 - can replace, -1 can't replace
        if check_invoice_result == 0:
            self.logger.debug('Инвойс валидный, можно проверять')
            result = await self.check_invoice_payment(that_invoice)
            return result
        elif check_invoice_result == -1:
            self.logger.debug('Инвойс чужой')
            return 'other'
        elif check_invoice_result == 1:
            self.logger.debug('Инвойс или просрочен, или исполнен')
            return 'exceeded'
        else:
            self.logger.debug('Инвойс какой-то странный')
            return 'weird'

    async def check_invoice_payment(self, invoice):
        link = f'https://api.trongrid.io/v1/accounts/{self.publ_key}/transactions/trc20?&contract_address={self.usdt_address}&limit=10'
        try:
            async with httpx.AsyncClient() as client:
                r = await client.get(link)
                self.logger.debug(
                    'Проверка с тронскана прислала: '+str(r.json()))
                data = r.json()['data']

                async def check_transaction(transaction, invoice):
                    transaction_time = transaction['block_timestamp']
                    transaction_type = transaction['type']
                    transaction_id = transaction['transaction_id']
                    transaction_token_address = transaction['token_info']['address']
                    transaction_amount = transaction['value']
                    if transaction_type != 'Transfer':
                        self.logger.debug(
                            f"Транзакция {transaction_id} пришла не тем способом {transaction_type}")
                        return 0
                    if transaction_time > invoice['timestamp_deadline'] or transaction_time < invoice['timestamp_issued']:
                        self.logger.debug(
                            f"Транзакция {transaction_id} пришла не в свое время ({invoice['timestamp_issued']},{invoice['timestamp_deadline']})")
                        return 0
                    if transaction_token_address != self.usdt_address:
                        self.logger.debug(
                            f'Транзакция {transaction_id} пришла не со своим токеном')
                        return 0
                    if transaction_amount != invoice['amount']:
                        self.logger.debug(
                            f'Транзакция {transaction_id} пришла не с неверным числом денег {transaction_amount}')
                        return 0
                    self.logger.debug(
                        f'Транзакция {transaction_id} вроде нормальная')
                    transaction_amount_int = 0
                    invoice['completed'] = True

                    try:
                        transaction_amount_int = int(
                            int(transaction_amount)/10**self.DECIMALS)
                    except:
                        self.logger.debug(
                            f'Ошибка конвертации суммы {transaction_amount} в число')
                        return 0
                    self.logger.debug(
                        f'Транзакция {transaction_id} будет давать вот столько денег: {transaction_amount_int}')
                    return transaction_amount_int

                results = await asyncio.gather(*[check_transaction(transaction, invoice)
                                                 for transaction in data])
                amount = max(results, default=0)
                return amount
        except Exception as err:
            self.logger.debug('Ошибка при проверке инвойса: '+str(err))
        return 0
